Conversation
There was a problem hiding this comment.
Pull request overview
Adds post-commissioning configuration for Matter devices in the Matter extension by attempting to identify the newly commissioned device and renaming it via Home Assistant’s device registry APIs.
Changes:
- Capture device registry state before commissioning and use it to identify devices added/updated during commissioning.
- After commissioning, locate the commissioned Matter device and rename it using a new device-registry update request.
- Project file updates (path/comment adjustments for some test files and local package reference label changes).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| Sources/Extensions/Matter/MatterRequestHandler.swift | Tracks commissioning context, diffs device registry, identifies a Matter device, and renames it after commissioning. |
| Sources/Extensions/Matter/HATypedRequest+Matter.swift | Adds typed requests/models to list config entries and update a device registry entry name. |
| HomeAssistant.xcodeproj/project.pbxproj | Project metadata updates (file reference comment/path display tweaks and build phase list fields). |
| guard let matterDevice = addedDevices.first(where: { device in | ||
| isMatterDevice(device, matterConfigEntryIDs: matterConfigEntryIDs) | ||
| }) else { | ||
| Current.Log.verbose("No Matter device was found among the newly added devices") | ||
| return | ||
| } | ||
|
|
||
| try await renameDevice( | ||
| id: matterDevice.id, | ||
| to: name, | ||
| using: connection | ||
| ) | ||
| Current.Log.info("Renamed commissioned Matter device \(matterDevice.id) to \(name)") |
There was a problem hiding this comment.
configureDevice picks the first device in addedDevices that matches isMatterDevice(...). If multiple Matter devices are added/updated during commissioning (e.g., bridges/multi-endpoint devices or concurrent commissioning), this will rename an arbitrary candidate depending on registry ordering. Consider collecting all matching Matter candidates and only renaming when the match is unambiguous (exactly one), otherwise log and skip (or apply a deterministic selection heuristic, e.g., newest created/modified timestamp).
| guard let matterDevice = addedDevices.first(where: { device in | |
| isMatterDevice(device, matterConfigEntryIDs: matterConfigEntryIDs) | |
| }) else { | |
| Current.Log.verbose("No Matter device was found among the newly added devices") | |
| return | |
| } | |
| try await renameDevice( | |
| id: matterDevice.id, | |
| to: name, | |
| using: connection | |
| ) | |
| Current.Log.info("Renamed commissioned Matter device \(matterDevice.id) to \(name)") | |
| let matterDevices = addedDevices.filter { device in | |
| isMatterDevice(device, matterConfigEntryIDs: matterConfigEntryIDs) | |
| } | |
| switch matterDevices.count { | |
| case 0: | |
| Current.Log.verbose("No Matter device was found among the newly added devices") | |
| return | |
| case 1: | |
| let matterDevice = matterDevices[0] | |
| try await renameDevice( | |
| id: matterDevice.id, | |
| to: name, | |
| using: connection | |
| ) | |
| Current.Log.info("Renamed commissioned Matter device \(matterDevice.id) to \(name)") | |
| default: | |
| Current.Log.warning( | |
| """ | |
| Skipping Matter device rename because multiple Matter candidates were found after commissioning: \ | |
| \(matterDevices.map(deviceNameForLogging(_:))) | |
| """ | |
| ) | |
| return | |
| } |
| let earliestRelevantTimestamp = commissioningStartedAt.timeIntervalSince1970 - 60 | ||
| return after.filter { device in | ||
| max(device.createdAt ?? 0, device.modifiedAt ?? 0) >= earliestRelevantTimestamp | ||
| } |
There was a problem hiding this comment.
The timestamp-based fallback in devicesAddedDuringCommissioning can include devices modified long after commissioning started (it filters everything with created/modified >= start-60s). If configureDevice runs later or other devices change during that period, it expands the candidate set and increases the chance of renaming the wrong device. Consider bounding the fallback window (e.g., store a commissioningCompletedAt when the commission call returns and filter within a limited interval) and/or requiring a single unambiguous Matter candidate before renaming.
| private func fetchDeviceRegistry(using connection: HAConnection) async throws -> [DeviceRegistryEntry] { | ||
| try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<[DeviceRegistryEntry], Error>) in | ||
| connection.send(.configDeviceRegistryList()).promise.pipe { result in | ||
| switch result { | ||
| case let .fulfilled(entries): | ||
| continuation.resume(returning: entries) | ||
| case let .rejected(error): | ||
| continuation.resume(throwing: error) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private func fetchMatterConfigEntryIDs(using connection: HAConnection) async throws -> Set<String> { | ||
| let configEntries = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation< | ||
| [MatterConfigEntry], | ||
| Error | ||
| >) in | ||
| connection.send(.configEntriesList()).promise.pipe { result in | ||
| switch result { | ||
| case let .fulfilled(entries): | ||
| continuation.resume(returning: entries) | ||
| case let .rejected(error): | ||
| continuation.resume(throwing: error) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return Set(configEntries.filter { $0.domain == "matter" }.map(\.entryId)) | ||
| } | ||
|
|
||
| private func renameDevice( | ||
| id: String, | ||
| to name: String, | ||
| using connection: HAConnection | ||
| ) async throws { | ||
| try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in | ||
| connection.send(.updateDeviceRegistry(deviceId: id, nameByUser: name)).promise.pipe { result in | ||
| switch result { | ||
| case .fulfilled: | ||
| continuation.resume(returning: ()) | ||
| case let .rejected(error): | ||
| continuation.resume(throwing: error) | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
fetchDeviceRegistry, fetchMatterConfigEntryIDs, and renameDevice wrap PromiseKit callbacks in withCheckedThrowingContinuation, but this file already uses PromiseKit’s async() bridge (e.g., in commissionDevice). To reduce boilerplate and avoid continuation misuse (double-resume, leaks on early returns), consider awaiting the underlying Promise directly (e.g., try await connection.send(...).promise.async()).
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4515 +/- ##
=======================================
Coverage ? 42.40%
=======================================
Files ? 274
Lines ? 16316
Branches ? 0
=======================================
Hits ? 6918
Misses ? 9398
Partials ? 0 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Summary
Screenshots
Link to pull request in Documentation repository
Documentation: home-assistant/companion.home-assistant#
Any other notes